Skip to content

fix(pg): do not treat Sync as connection ending - #3772

Merged
brianc merged 3 commits into
brianc:masterfrom
devtechedge:fix/connection-sync-ending-flag
Sep 11, 2026
Merged

fix(pg): do not treat Sync as connection ending#3772
brianc merged 3 commits into
brianc:masterfrom
devtechedge:fix/connection-sync-ending-flag

Conversation

@devtechedge

Copy link
Copy Markdown
Contributor

Summary

Fixes #3769.

Connection.sync() was setting _ending = true on every extended-query Sync. That flag exists so reportStreamError can ignore ECONNRESET / EPIPE during disconnect. Sync is the protocol barrier after Parse/Bind/Execute, not a disconnect, so after the first parameterized query those socket errors were silently dropped for the rest of the connection lifetime.

With pipeline: true that interacts badly with unexpected pooler/socket teardowns: the normal error path is closed and recovery depends only on the async close/end path, which can leave an in-flight query promise unsettled.

Changes

  • Stop setting _ending in sync(); leave it set only in end() (Terminate) and the connect-timeout teardown path that already sets con._ending = true before destroying the stream.
  • Add unit coverage that Sync leaves _ending false and that ECONNRESET after Sync still emits error (existing disconnect coverage still uses end()).

Test plan

  • node test/unit/connection/error-tests.js (new cases green)
  • Full packages/pg unit suite (find test/unit -name '*-tests.js' | xargs -n1 node) - 284 pass

Connection.sync() was setting _ending=true on every extended-query Sync.
That flag is meant for disconnect (Terminate / end()), so after the first
parameterized query ECONNRESET and EPIPE were swallowed for the life of
the connection. Keep _ending only on end() and connect-timeout teardown.

Fixes brianc#3769

@brianc brianc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah I see this fix makes sense. What I want to see tho is an integration tests w/ an actual postgres backend to trigger the error path if possible. Unit tests are a pretty weak gaurentee of correctness compared to integration tests which actually test "This code works now against a backend and it didn't use to before the patch." Would you be able to include an integration test? (under packages/pg/test/integration/gh-issues/3772-tests.js if possible)

devtechedge added a commit to devtechedge/node-postgres that referenced this pull request Sep 10, 2026
devtechedge added a commit to devtechedge/node-postgres that referenced this pull request Sep 10, 2026
devtechedge added a commit to devtechedge/node-postgres that referenced this pull request Sep 10, 2026
Adds a real-backend integration test under test/integration/gh-issues/
covering the bug from brianc#3769: Connection.prototype.sync() used to set
_ending = true, so every healthy extended-protocol connection was left
looking like it was ending. reportStreamError drops ECONNRESET/EPIPE
while _ending is set, so a genuine mid-query teardown was silently
swallowed and only the generic close-path error surfaced.

The tests use a real PostgreSQL backend reached through a local TCP
proxy, so the connection reset can be triggered deterministically:

  1. a real extended-protocol query does not mark the connection as
     ending (fails before the fix)
  2. a mid-query connection reset is reported, not swallowed by Sync
     (fails before the fix: no ECONNRESET reaches the client)

Both tests fail on the pre-patch code and pass with the fix.
@devtechedge

Copy link
Copy Markdown
Contributor Author

Added the integration test you asked for at packages/pg/test/integration/gh-issues/3772-tests.js.

It runs both cases against a real Postgres backend:

  1. a real extended-protocol query does not mark the connection as ending - a parameterized query really does write Sync, so _ending has to stay false for the life of a healthy connection.
  2. a mid-query connection reset is reported, not swallowed by Sync - the client reaches Postgres through a local TCP proxy, the query is in flight, then the proxy socket is reset.

I verified the "it didn't use to" part by swapping only packages/pg/lib/connection.js between the patched and pre-patch versions, with everything else untouched:

  • case 1: fails before the patch, passes after
  • case 2: fails before the patch, passes after

Before the fix, case 2 only surfaces the generic Connection terminated unexpectedly from the close path. With the fix the underlying ECONNRESET reaches the client.

Local runs:

  • find test/integration/gh-issues -name '*-tests.js' | xargs -n1 node
  • node test/unit/connection/error-tests.js
  • eslint and prettier clean on the new file

Two other integration files fail on my machine for environment reasons, not from this change: 130-tests.js needs a psql binary on PATH, and the SSL tests need a server started with SSL. Both fail identically without the patch.

Happy to rework the teardown approach if you'd prefer it driven a different way.

The integration suite runs twice: once with the JS implementation and
once with a `native` argument that swaps in the libpq bindings. The
native client has no `connection` (and therefore no `_ending`), so the
state-machine assertions threw a TypeError and aborted the run via the
helper's uncaughtException handler.

Guard on helper.args.native, matching the existing idiom in
test/integration/client/pipeline-portal-tests.js.
@devtechedge

Copy link
Copy Markdown
Contributor Author

One follow-up on my last comment: the first push of the integration test turned CI red, and I've fixed it.

The integration suite runs twice, once against the JS implementation and once with a native argument that swaps in the libpq bindings. The native client has no connection, so my state-machine assertions threw a TypeError and aborted the run. I've guarded the file the same way test/integration/client/pipeline-portal-tests.js does, since the patch is to the JS Connection and there is no _ending to assert on under libpq.

CI is now green across all 12 checks (Node 16 through 26, PostgreSQL 13 through 18, plus lint).

@brianc brianc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! with the integration test I feel 💯 about merging this, thanks. I think its probably time I write an AGENTS.md in the repo to give instructions to folks who are using agents to make sure to include integration tests when possible as unit tests are generally far less valuable.

@brianc
brianc merged commit 9683053 into brianc:master Sep 11, 2026
12 checks passed
@brianc

brianc commented Sep 11, 2026

Copy link
Copy Markdown
Owner

thank you for putting in the work here & the follow up! Appreciated!

@devtechedge

devtechedge commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for merging, Brian!

This PR turned out to be a real learning experience with the extended query protocol.

I went in thinking Sync was just a message boundary, and came out with a proper understanding of why it exists as the barrier after Parse/Bind/Execute, and how _ending works with reportStreamError to keep intentional disconnects from surfacing as errors.

The pipeline: true angle made it more interesting than it first looked, since the dropped ECONNRESET only really showed up through pooler teardowns.

Thanks as well for the quick review, and for maintaining a codebase that made tracing this straightforward. Happy to help with anything else that comes up in the repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants